package org.infodavid.common.lang;

import java.io.BufferedReader;
import java.io.Reader;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The Class ClassUtil.
 */
@SuppressWarnings(value = {
    "unchecked", "rawtypes"
})
public final class ClassUtil {

	/** The Constant ARGUMENTS_SEPARATOR. */
	public static final String ARGUMENTS_SEPARATOR = "\\|";

	/** The Constant CLASS_VALUE_SEPARATOR. */
	public static final String CLASS_VALUE_SEPARATOR = "=";

	/** The Constant DEFAULT_VALUES. */
	public static final Map<Class,Object> DEFAULT_VALUES;

	/** The Constant log. */
	private static final Log LOGGER = LogFactory.getLog(ClassUtil.class);// NOPMD

	/** The singleton. */
	private static WeakReference<ClassUtil> singleton = null;

	static {
		DEFAULT_VALUES = new HashMap<Class,Object>();
	}

	/**
	 * returns the singleton.
	 * @return the singleton
	 */
	public static ClassUtil getInstance() {
		if (singleton == null || singleton.get() == null) {
			singleton = new WeakReference<ClassUtil>(new ClassUtil());
		}

		return singleton.get();
	}

	/**
	 * Instantiates a new reflect utilities.
	 */
	private ClassUtil() {
		super();
	}

	/**
	 * create a new instance using an interface and the properties. It get the class of implementation
	 * using the name of the interface as the key in the map : MyInterface=MyClassOfImplementation
	 * @param <T> the type of the objet to instantiate
	 * @param prop the properties
	 * @param iface the interface
	 * @param defaultClassName the class name of the default implementation
	 * @return an object or null
	 */
	public <T> T buildInstance(final Properties prop, final Class<T> iface,
	    final String defaultClassName) {
		T obj = null;

		try {
			obj = newInstance(prop, iface, (Class)null, (Object)null);
		}
		catch (final Throwable e) {
			try {
				obj = (T)newInstance(defaultClassName);
			}
			catch (final Throwable e2) {
				throw new Error("Can't instantiate a new " + iface.getSimpleName()
				    + " using the class name: the property '" + iface.getName()
				    + "' used to specify the concrete class for this factory is invalid.", e2);
			}
		}

		return obj;
	}

	/**
	 * Gets the generic type.
	 * @param owner the owner
	 * @param name the name
	 * @return the generic type
	 */
	public Class getGenericType(final Object owner, final String name) {
		final TypeVariable[] variables = owner.getClass().getTypeParameters();
		Class result = null;

		if (!(variables == null || variables.length == 0)) {
			Type[] types;

			for (final TypeVariable item : variables) {
				if (name.equalsIgnoreCase(item.getName())) {
					types = item.getBounds();

					if (!(types == null || types.length == 0)) {
						if (types[0] instanceof Class) {
							result = (Class)types[0];
						}
					}

					break;
				}
			}
		}

		return result;
	}

	/**
	 * Handle.
	 * @param error the error
	 * @throws InstantiationException the instantiation exception
	 */
	private void handle(final Throwable error) throws InstantiationException {
		if (error instanceof InstantiationException) {
			throw (InstantiationException)error;
		}

		LOGGER.error(error);

		throw new InstantiationException(error.getMessage());
	}

	/**
	 * New instance.
	 * @param <T> the type of the objet to instantiate
	 * @param cls the class
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Class<T> cls) throws InstantiationException {
		if (cls == null) {
			throw new InstantiationException("Class is null");
		}

		try {
			final Constructor<T> constructor = cls.getDeclaredConstructor();
			constructor.setAccessible(true);

			return constructor.newInstance();
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param <T> the type of the objet to instantiate
	 * @param cls the class
	 * @param argType the arguments type
	 * @param arg the arguments
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Class<T> cls, final Class argType, final Object arg)
	    throws InstantiationException {
		if (cls == null) {
			throw new InstantiationException("Class is null");
		}

		try {
			final Constructor<T> constructor = cls.getDeclaredConstructor(argType);
			constructor.setAccessible(true);

			return constructor.newInstance(arg);
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param <T> the type of the objet to instantiate
	 * @param cls the class
	 * @param argsTypes the arguments types
	 * @param args the arguments
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Class<T> cls, final Class[] argsTypes, final Object[] args)
	    throws InstantiationException {
		if (cls == null) {
			throw new InstantiationException("Class is null");
		}

		try {
			final Constructor<T> constructor = cls.getDeclaredConstructor(argsTypes);
			constructor.setAccessible(true);

			return constructor.newInstance(args);
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param <T> the type of the objet to instantiate
	 * @param cls the class
	 * @param arg the arguments
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Class<T> cls, final Object arg) throws InstantiationException {
		if (cls == null) {
			throw new InstantiationException("Class is null");
		}

		try {
			final Constructor<T> constructor = cls.getDeclaredConstructor(arg.getClass());
			constructor.setAccessible(true);

			return constructor.newInstance(arg);
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param <T> the type of the objet to instantiate
	 * @param parent the parent
	 * @param cls the class
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Object parent, final Class<T> cls) throws InstantiationException {
		if (parent == null) {
			throw new InstantiationException("Parent object for inner class is null");
		}
		else if (cls == null) {
			throw new InstantiationException("Class is null");
		}
		else {
			Constructor<T> constructor = null;

			try {
				constructor = cls.getDeclaredConstructor();
				constructor.setAccessible(true);

				return constructor.newInstance();
			}
			catch (final Exception e) {
				try {
					constructor = cls.getDeclaredConstructor(parent.getClass());
					constructor.setAccessible(true);

					return constructor.newInstance(parent);
				}
				catch (final Exception e2) {
					LOGGER.warn("An error occurs while catching the error", e2);

					handle(e);

					return null;
				}
			}
		}
	}

	/**
	 * create a new instance using an interface and the properties. It get the class of implementation
	 * using the name of the interface as the key in the map : MyInterface=MyClassOfImplementation
	 * @param <T> the type of the objet to instantiate
	 * @param prop the properties
	 * @param iface the interface
	 * @return an object or null
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Properties prop, final Class<T> iface)
	    throws InstantiationException {
		return newInstance(prop, iface, (Class)null, (Object)null);
	}

	/**
	 * create a new instance using an interface and the properties. It get the class of implementation
	 * using the name of the interface as the key in the map : MyInterface=MyClassOfImplementation
	 * @param <T> the type of the objet to instantiate
	 * @param prop the properties
	 * @param iface the interface
	 * @param argsType the argument type
	 * @param arg the argument
	 * @return an object or null
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Properties prop, final Class<T> iface, final Class argsType,
	    final Object arg) throws InstantiationException {
		if (prop == null) {
			throw new InstantiationException("Properties object is null");
		}
		else if (iface == null) {
			throw new InstantiationException("Class object is null");
		}
		else {
			final String key = iface.getName();
			String property = prop.getProperty(key);
			T o;

			if (property == null) {
				property = System.getProperty(key);
			}

			if (property == null) {
				throw new InstantiationException("Can find implementation class for: " + key);
			}
			else if (!(argsType == null || arg == null)) {// NOPMD
				o = (T)newInstance(property, argsType, arg);
			}
			else {
				o = (T)newInstance(property);
			}

			return o;
		}
	}

	/**
	 * create a new instance using an interface and the properties. It get the class of implementation
	 * using the name of the interface as the key in the map : MyInterface=MyClassOfImplementation
	 * @param <T> the type of the objet to instantiate
	 * @param prop the properties
	 * @param iface the interface
	 * @param argsTypes the arguments types
	 * @param args the arguments
	 * @return an object or null
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Properties prop, final Class<T> iface, final Class[] argsTypes,
	    final Object[] args) throws InstantiationException {
		if (prop == null) {
			throw new InstantiationException("Properties object is null");
		}
		else if (iface == null) {
			throw new InstantiationException("Class object is null");
		}
		else {
			final String key = iface.getName();
			String property = prop.getProperty(key);
			T o;

			if (property == null) {
				property = System.getProperty(key);
			}

			if (property == null) {
				throw new InstantiationException("Can find implementation class for: " + key);
			}
			else if (!(argsTypes == null || args == null || argsTypes.length != args.length)) {// NOPMD
				o = (T)newInstance(property, argsTypes, args);
			}
			else {
				o = (T)newInstance(property);
			}

			return o;
		}
	}

	/**
	 * create a new instance using an interface and the properties. It get the class of implementation
	 * using the name of the interface as the key in the map : MyInterface=MyClassOfImplementation
	 * @param <T> the type of the objet to instantiate
	 * @param prop the properties
	 * @param iface the interface
	 * @param arg the argument
	 * @return an object or null
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Properties prop, final Class<T> iface, final Object arg)
	    throws InstantiationException {
		if (prop == null) {
			throw new InstantiationException("Properties object is null");
		}
		else if (iface == null) {
			throw new InstantiationException("Class object is null");
		}
		else {
			final String key = iface.getName();
			String property = prop.getProperty(key);
			T o;

			if (property == null) {
				property = System.getProperty(key);
			}

			if (property == null) {
				throw new InstantiationException("Can find implementation class for: " + key);
			}
			else if (arg != null) {// NOPMD
				o = (T)newInstance(property, arg.getClass(), arg);
			}
			else {
				o = (T)newInstance(property);
			}

			return o;
		}
	}

	/**
	 * create a new instance using an interface and the properties. It get the class of implementation
	 * using the name of the interface as the key in the map : MyInterface=MyClassOfImplementation
	 * @param <T> the type of the objet to instantiate
	 * @param prop the properties
	 * @param iface the interface
	 * @param defaultClassName the class name of the default implementation
	 * @return an object or null
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Properties prop, final Class<T> iface,
	    final String defaultClassName) throws InstantiationException {
		T obj = newInstance(prop, iface, (Class)null, (Object)null);

		if (obj == null) {
			obj = (T)newInstance(defaultClassName);
		}

		return obj;
	}

	/**
	 * New instance.
	 * @param <T> the type of the objet to instantiate
	 * @param reader the reader
	 * @param cls the class
	 * @return the instance
	 * @throws InstantiationException the instantiation exception
	 */
	public <T> T newInstance(final Reader reader, final Class<T> cls) throws InstantiationException {
		if (reader == null) {
			throw new InstantiationException("Stream is null");
		}
		else if (cls == null) {
			throw new InstantiationException("Class is null");
		}
		else {
			final BufferedReader br = new BufferedReader(reader);
			final String className = cls.getName();
			String line;
			String[] parts;
			T obj = null;

			try {
				while (obj == null && (line = br.readLine()) != null) {// NOPMD
					parts = line.trim().split("=");

					if (parts.length > 1 && className.equals(parts[0])) {
						obj = (T)newInstance(parts[1]);
					}
				}
			}
			catch (final Exception e) {
				handle(e);

				return null;
			}

			return obj;
		}
	}

	/**
	 * Call the default constructor for each line describing the class name.
	 * @param reader the is
	 * @param objects the objects
	 * @throws InstantiationException the instantiation exception
	 */
	public void newInstance(final Reader reader, final List<Object> objects)
	    throws InstantiationException {
		final BufferedReader br = new BufferedReader(reader);
		String s = null;

		try {
			while ((s = br.readLine()) != null) {
				objects.add(newInstance(Class.forName(s.trim())));
			}
		}
		catch (final Exception e) {
			handle(e);
		}
	}

	/**
	 * New instance.
	 * @param value the value
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public Object newInstance(final String value) throws InstantiationException {
		if (value == null) {
			throw new InstantiationException("Class full qualified name is null");
		}

		try {
			return newInstance(Class.forName(value));
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param value the value
	 * @param cls the class of the argument
	 * @param obj the argument
	 * @return the object
	 * @throws InstantiationException the instantiation exception
	 */
	public Object newInstance(final String value, final Class cls, final Object obj)
	    throws InstantiationException {
		if (value == null) {
			throw new InstantiationException("Class full qualified name is null");
		}

		try {
			return newInstance(Class.forName(value), cls, obj);
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param cls the class
	 * @param argsTypes the arguments types
	 * @param args the arguments
	 * @return <code>Object</code>
	 * @throws InstantiationException the instantiation exception
	 */
	public Object newInstance(final String cls, final Class[] argsTypes, final Object[] args)
	    throws InstantiationException {
		if (cls == null) {
			throw new InstantiationException("Class full qualified name is null");
		}

		try {
			final Constructor constructor = Class.forName(cls).getDeclaredConstructor(argsTypes);

			constructor.setAccessible(true);

			return constructor.newInstance(args);
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instance.
	 * @param value the value
	 * @param obj the argument
	 * @return the object
	 * @throws InstantiationException the instantiation exception
	 */
	public Object newInstance(final String value, final Object obj) throws InstantiationException {
		if (value == null) {
			throw new InstantiationException("Class full qualified name is null");
		}

		try {
			return newInstance(Class.forName(value), obj);
		}
		catch (final Exception e) {
			handle(e);

			return null;
		}
	}

	/**
	 * New instances.
	 * @param reader the input stream
	 * @param objects the objects
	 * @return the collection< object>
	 * @throws InstantiationException the instantiation exception
	 */
	public Collection<Object> newInstances(final Reader reader, final Collection<Object> objects)
	    throws InstantiationException {
		if (reader == null) {
			throw new InstantiationException("Stream is null");
		}

		final Collection<Object> results = objects == null ? new ArrayList<Object>() : objects;
		final BufferedReader br = new BufferedReader(reader);
		String s = null;

		try {
			while ((s = br.readLine()) != null) {
				results.add(newInstance(Class.forName(s.trim())));
			}
		}
		catch (final Exception e) {
			handle(e);
		}

		return results;
	}

	/**
	 * New instances.
	 * @param value the value
	 * @param objects the objects
	 * @return the list< object>
	 * @throws InstantiationException the instantiation exception
	 */
	public Collection<Object> newInstances(final String value, final Collection<Object> objects)
	    throws InstantiationException {
		if (value == null) {
			throw new InstantiationException("Class full qualified name is null");
		}

		final String[] names = value.split(";");
		final Collection<Object> results = objects == null ? new ArrayList<Object>() : objects;

		if (names != null) {
			final int count = names.length;

			try {
				for (short i = 0; i < count; i++) {
					results.add(newInstance(Class.forName(names[i])));
				}
			}
			catch (final Exception e) {
				handle(e);
			}
		}

		return results;
	}
}
